/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.sword;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Date;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Collection;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.CollectionService;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.core.Utils;
import org.purl.sword.base.Deposit;
import org.purl.sword.base.DepositResponse;
import org.purl.sword.base.SWORDAuthenticationException;
import org.purl.sword.base.SWORDEntry;
import org.purl.sword.base.SWORDErrorException;

/**
 * This class is responsible for initiating the process of
 * deposit of SWORD Deposit objects into the DSpace repository
 *
 * @author Richard Jones
 */
public class DepositManager {
    /**
     * Log4j logger
     */
    public static final Logger log = org.apache.logging.log4j.LogManager.getLogger(DepositManager.class);

    /**
     * The SWORD service implementation
     */
    private SWORDService swordService;

    /**
     * Construct a new DepositManager using the given instantiation of
     * the SWORD service implementation
     *
     * @param service SWORD service
     */
    public DepositManager(SWORDService service) {
        this.swordService = service;
        log.debug("Created instance of DepositManager");
    }

    public DSpaceObject getDepositTarget(Deposit deposit)
        throws DSpaceSWORDException, SWORDErrorException {
        SWORDUrlManager urlManager = swordService.getUrlManager();
        Context context = swordService.getContext();

        // get the target collection
        String loc = deposit.getLocation();
        DSpaceObject dso = urlManager.getDSpaceObject(context, loc);

        swordService.message("Performing deposit using location: " + loc);

        if (dso instanceof Collection) {
            CollectionService collectionService = ContentServiceFactory
                .getInstance().getCollectionService();
            swordService.message(
                "Location resolves to collection with handle: " +
                    dso.getHandle() +
                    " and name: " +
                    collectionService.getName((Collection) dso));
        } else if (dso instanceof Item) {
            swordService.message("Location resolves to item with handle: " +
                                     dso.getHandle());
        }

        return dso;
    }

    /**
     * Once this object is fully prepared, this method will execute
     * the deposit process.  The returned DepositRequest can be
     * used then to assemble the SWORD response.
     *
     * @param deposit deposit request
     * @return the response to the deposit request
     * @throws DSpaceSWORDException         can be thrown by the internals of the DSpace SWORD implementation
     * @throws SWORDErrorException          on generic SWORD exception
     * @throws SWORDAuthenticationException Thrown if the authentication fails
     */
    public DepositResponse deposit(Deposit deposit)
        throws DSpaceSWORDException, SWORDErrorException,
        SWORDAuthenticationException {
        // start the timer, and initialise the verboseness of the request
        Date start = new Date();
        swordService.message("Initialising verbose deposit");

        // get the things out of the service that we need
        SWORDContext swordContext = swordService.getSwordContext();
        Context context = swordService.getContext();

        // get the deposit target
        DSpaceObject dso = this.getDepositTarget(deposit);

        // find out if the supplied SWORDContext can submit to the given
        // dspace object
        SWORDAuthenticator auth = new SWORDAuthenticator();
        if (!auth.canSubmit(swordService, deposit, dso)) {
            // throw an exception if the deposit can't be made
            String oboEmail = "none";
            if (swordContext.getOnBehalfOf() != null) {
                oboEmail = swordContext.getOnBehalfOf().getEmail();
            }
            log.info(LogManager.getHeader(context,
                                          "deposit_failed_authorisation",
                                          "user=" + swordContext.getAuthenticated().getEmail() +
                                              ",on_behalf_of=" + oboEmail));
            throw new SWORDAuthenticationException(
                "Cannot submit to the given collection with this context");
        }

        // make a note of the authentication in the verbose string
        swordService.message("Authenticated user: " +
                                 swordContext.getAuthenticated().getEmail());
        if (swordContext.getOnBehalfOf() != null) {
            swordService.message("Depositing on behalf of: " +
                                     swordContext.getOnBehalfOf().getEmail());
        }

        // determine which deposit engine we initialise
        Depositor dep = null;
        if (dso instanceof Collection) {
            swordService.message(
                "Initialising depositor for an Item in a Collection");
            dep = new CollectionDepositor(swordService, dso);
        } else if (dso instanceof Item) {
            swordService.message(
                "Initialising depositor for a Bitstream in an Item");
            dep = new ItemDepositor(swordService, dso);
        }

        if (dep == null) {
            log.error(
                "The specified deposit target does not exist, or is not a collection or an item");
            throw new DSpaceSWORDException(
                "Deposit target is not a collection or an item");
        }

        DepositResult result = null;

        try {
            result = dep.doDeposit(deposit);
        } catch (DSpaceSWORDException | SWORDErrorException | RuntimeException e) {
            if (swordService.getSwordConfig().isKeepPackageOnFailedIngest()) {
                try {
                    storePackageAsFile(deposit);
                } catch (IOException e2) {
                    log.warn("Unable to store SWORD package as file: " + e);
                }
            }
            throw e;
        }

        // now construct the deposit response.  The response will be
        // CREATED if the deposit is in the archive, or ACCEPTED if
        // the deposit is in the workflow.  We use a separate record
        // for the handle because DSpace will not supply the Item with
        // a record of the handle straight away.
        String handle = result.getHandle();
        int state = Deposit.CREATED;
        if (StringUtils.isBlank(handle)) {
            state = Deposit.ACCEPTED;
        }

        DepositResponse response = new DepositResponse(state);
        response.setLocation(result.getMediaLink());

        DSpaceATOMEntry dsatom = null;
        if (result.getItem() != null) {
            swordService.message("Initialising ATOM entry generator for an Item");
            dsatom = new ItemEntryGenerator(swordService);
        } else if (result.getBitstream() != null) {
            swordService.message(
                "Initialising ATOM entry generator for a Bitstream");
            dsatom = new BitstreamEntryGenerator(swordService);
        }
        if (dsatom == null) {
            log.error("The deposit failed, see exceptions for explanation");
            throw new DSpaceSWORDException(
                "Result of deposit did not yield an Item or a Bitstream");
        }
        SWORDEntry entry = dsatom.getSWORDEntry(result, deposit);

        // if this was a no-op, we need to remove the files we just
        // deposited, and abort the transaction
        if (deposit.isNoOp()) {
            dep.undoDeposit(result);
            swordService.message(
                "NoOp Requested: Removed all traces of submission");
        }

        entry.setNoOp(deposit.isNoOp());

        Date finish = new Date();
        long delta = finish.getTime() - start.getTime();
        swordService.message(
            "Total time for deposit processing: " + delta + " ms");
        entry.setVerboseDescription(
            swordService.getVerboseDescription().toString());

        response.setEntry(entry);

        return response;
    }

    /**
     * Store original package on disk and companion file containing SWORD
     * headers as found in the deposit object
     * Also write companion file with header info from the deposit object.
     *
     * @param deposit the original deposit request
     */
    private void storePackageAsFile(Deposit deposit) throws IOException {
        String path = swordService.getSwordConfig().getFailedPackageDir();

        File dir = new File(path);
        if (!dir.exists() || !dir.isDirectory()) {
            throw new IOException(
                "Directory does not exist for writing packages on ingest error.");
        }

        String filenameBase =
            "sword-" + deposit.getUsername() + "-" + (new Date()).getTime();
        // No dots or slashes allowed in filename
        filenameBase = filenameBase.replaceAll("\\.", "").replaceAll("/", ""). replaceAll("\\\\", "");

        File packageFile = new File(path, filenameBase);
        File headersFile = new File(path, filenameBase + "-headers");

        InputStream is = new BufferedInputStream(
            new FileInputStream(deposit.getFile()));
        OutputStream fos = new BufferedOutputStream(
            new FileOutputStream(packageFile));
        Utils.copy(is, fos);
        fos.close();
        is.close();

        //write companion file with headers
        PrintWriter pw = new PrintWriter(
            new BufferedWriter(new FileWriter(headersFile)));

        pw.println("Content-Disposition=" + deposit.getContentDisposition());
        pw.println("Content-Type=" + deposit.getContentType());
        pw.println("Packaging=" + deposit.getPackaging());
        pw.println("Location=" + deposit.getLocation());
        pw.println("On Behalf of=" + deposit.getOnBehalfOf());
        pw.println("Slug=" + deposit.getSlug());
        pw.println("User name=" + deposit.getUsername());
        pw.close();
    }
}
